package eztools

import (
	"bytes"
	"encoding/json"
	"errors"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/rkl-/digest"
)

const (
	AUTH_NONE = iota
	AUTH_PLAIN
	AUTH_BASIC
	AUTH_DIGEST
	METHOD_GET  = "GET"
	METHOD_PUT  = "PUT"
	METHOD_POST = "POST"
	METHOD_DEL  = "DELETE"
)

type AuthInfo struct {
	Type       int
	User, Pass string
}

func digestAuth(user, pass string, req *http.Request) (*http.Response, error) {
	return digest.NewTransport(user, pass).RoundTrip(req)
}

func genFile(defRdr io.Reader, fType, fName string) (string, io.Reader) {
	if len(fName) < 1 {
		if len(fType) < 1 {
			return "application/json; charset=utf-8", defRdr
		}
		return fType, defRdr
	}
	if len(fType) < 1 {
		fType = "file"
	}
	var err error
	defer func() {
		if err != nil {
			LogErrPrint(err)
		}
	}()
	// New multipart writer.
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	fw, err := writer.CreateFormFile(fType, filepath.Base(fName))
	if err != nil {
		return fType, defRdr
	}
	file, err := os.Open(fName)
	if err != nil {
		return fType, defRdr
	}
	_, err = io.Copy(fw, file)
	if err != nil {
		return fType, defRdr
	}
	writer.Close()
	return writer.FormDataContentType(), bytes.NewReader(body.Bytes())
}

func genReq(method, url string, bodyReq io.Reader,
	fType, fName string, hdrs map[string]string) (req *http.Request, err error) {
	bodyType, bodyReq := genFile(bodyReq, fType, fName)
	req, err = http.NewRequest(method, url, bodyReq)
	if err != nil {
		if Debugging {
			ShowStrln("failed to create " + method)
		}
		return
	}
	if bodyReq != nil {
		req.Header.Add("Content-Type", bodyType)
		if Debugging && Verbose > 2 {
			ShowStrln("body type=" + bodyType)
		}
	}
	req.Header.Set("Accept", "application/json, text/*; charset=utf-8")
	for n, v := range hdrs {
		req.Header.Add(n, v)
		if Debugging && Verbose > 2 {
			ShowStrln("adding header:" + n + "=" + v)
		}
	}
	return
}

func parseBody(resp *http.Response, magic []byte) (bodyType string, bodyBytes []byte, errNo int, err error) {
	defer resp.Body.Close()

	errNo = resp.StatusCode
	if errNo < http.StatusOK || errNo >= http.StatusBadRequest {
		if Debugging && Verbose > 1 {
			ShowStrln("failure response " + strconv.Itoa(errNo))
		}
		var b []byte
		if resp.ContentLength > 0 {
			b = make([]byte, resp.ContentLength)
			resp.Body.Read(b)
		}
		return "", b, errNo, errors.New(resp.Status)
	}

	if Debugging && Verbose > 2 {
		ShowStrln("resp code=" + strconv.Itoa(errNo))
	}
	if cl := resp.Header.Get("Content-Length"); cl == "0" {
		if Debugging && Verbose > 2 {
			ShowStrln("no body in response")
		}
		return
	}
	bodyBytes, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		LogErrPrint(err)
		return
	}
	if bodyBytes == nil || len(bodyBytes) < 1 {
		if Debugging && Verbose > 2 {
			ShowStrln("no body")
		}
		return
	}
	if len(magic) > 0 {
		if Debugging && Verbose > 1 {
			ShowStrln("stripping magic")
		}
		if bytes.HasPrefix(bodyBytes, magic) {
			bodyBytes = bytes.TrimLeft(bytes.TrimPrefix(bodyBytes, magic), "\n\r")
		} else {
			err = errors.New("Magic not matched")
			return
		}
	}
	bodyType = resp.Header.Get("Content-Type")
	return
}

func parseResp(resp *http.Response, magic []byte) (body interface{}, errNo int, err error) {
	//var bodyMp map[string]interface{}
	bodyType, bodyBytes, errNo, err := parseBody(resp, magic)
	if err != nil || (bodyBytes == nil || len(bodyBytes) < 1) {
		return bodyBytes, errNo, err
	}
	switch {
	case strings.Contains(bodyType, "application/json"):
		err = json.Unmarshal(bodyBytes, &body)
		//fullResponse := successResponse{
		//Data: v,
		//}
		//if err = json.NewDecoder(resp.Body).Decode(&fullResponse); err != nil {
		//return
		//}
	case strings.Contains(bodyType, "text/plain"):
		body = bodyBytes
	default:
		return bodyBytes, errNo, errors.New("body type=" + bodyType)
	}
	if Debugging && Verbose > 2 {
		ShowSthln(body)
	}
	return
}

func restGetPB(method, url string, to time.Duration,
	bodyReq io.Reader, fType, fName string, hdrs map[string]string,
	fun func(req *http.Request)) (resp *http.Response, err error) {
	if Debugging && Verbose > 1 {
		ShowStrln(method + " " + url)
	}
	cli := &http.Client{
		Timeout: to,
	}
	req, err := genReq(method, url, bodyReq, fType, fName, hdrs)
	if err != nil {
		return
	}
	fun(req)
	//ShowSthln(req)
	//ShowSthln(req.ContentLength)
	resp, err = cli.Do(req)
	if err != nil {
		if Debugging {
			ShowStrln("failed to send/get")
		}
	}
	return
}

func restAddBasicAuth(req *http.Request, pass string) {
	if len(pass) > 0 {
		req.Header.Set("authorization", "Basic "+pass)
	}
}

// restGetPlain sends with timeout, Restful API request and returns the result.
func restGetPlainBodyBytes(method, url, pass string, to time.Duration, bodyReq io.Reader,
	magic []byte) (body []byte, errNo int, err error) {
	resp, err := restGetPB(method, url, to, bodyReq, "", "", nil,
		func(req *http.Request) {
			restAddBasicAuth(req, pass)
		})
	if err != nil {
		return
	}
	_, body, errNo, err = parseBody(resp, magic)
	return
}

func restGetPlain(method, url, pass string, to time.Duration, bodyReq io.Reader, magic []byte,
	fType, fName string, hdrs map[string]string) (bodyMap interface{}, errNo int, err error) {
	resp, err := restGetPB(method, url, to, bodyReq, fType, fName, hdrs,
		func(req *http.Request) {
			restAddBasicAuth(req, pass)
		})
	if err != nil {
		return
	}
	return parseResp(resp, magic)
}

// restGetBasic sends with timeout, Restful API request and returns the result.
func restGetBasicBodyBytes(method, url, user, pass string, to time.Duration, bodyReq io.Reader,
	magic []byte) (body []byte, errNo int, err error) {
	resp, err := restGetPB(method, url, to, bodyReq, "", "", nil,
		func(req *http.Request) {
			if len(pass) > 0 {
				req.SetBasicAuth(user, pass)
			}
		})
	if err != nil {
		return
	}
	_, body, errNo, err = parseBody(resp, magic)
	return
}

// restGetBasic sends with timeout, Restful API request and returns the result.
func restGetBasic(method, url, user, pass string, to time.Duration, bodyReq io.Reader, magic []byte,
	fType, fName string, hdrs map[string]string) (bodyMap interface{}, errNo int, err error) {
	resp, err := restGetPB(method, url, to, bodyReq, fType, fName, hdrs,
		func(req *http.Request) {
			if len(pass) > 0 {
				req.SetBasicAuth(user, pass)
			}

		})
	if err != nil {
		return
	}
	return parseResp(resp, magic)
}

const defRestGetTO = 60 * time.Second

func restGetDigestExec(method, url, user, pass string, bodyReq io.Reader,
	fType, fName string, hdrs map[string]string) (resp *http.Response, err error) {
	if Debugging && Verbose > 1 {
		ShowStrln(method + " " + url)
	}
	req, err := genReq(method, url, bodyReq, fType, fName, hdrs)
	if err != nil {
		return
	}
	resp, err = digestAuth(user, pass, req)
	if err != nil {
		if Debugging {
			ShowStrln("failed to send/get")
		}
	}
	return
}

func restGetDigestBodyBytes(method, url, user, pass string, bodyReq io.Reader,
	magic []byte) (body []byte, errNo int, err error) {
	resp, err := restGetDigestExec(method, url, user, pass, bodyReq, "", "", nil)
	if err != nil {
		return
	}
	_, body, errNo, err = parseBody(resp, magic)
	return
}

func restGetDigest(method, url, user, pass string, bodyReq io.Reader, magic []byte,
	fType, fName string, hdrs map[string]string) (bodyMap interface{}, errNo int, err error) {
	resp, err := restGetDigestExec(method, url, user, pass, bodyReq, fType, fName, hdrs)
	if err != nil {
		return
	}
	return parseResp(resp, magic)
}

func restGetOrPostBodyBytes(method, url string, authInfo AuthInfo, bodyReq io.Reader,
	magic []byte) (body []byte, errNo int, err error) {
	switch authInfo.Type {
	case AUTH_DIGEST:
		return restGetDigestBodyBytes(method, url, authInfo.User, authInfo.Pass, bodyReq, magic)
	case AUTH_NONE:
		return restGetPlainBodyBytes(method, url, "", defRestGetTO, bodyReq, magic)
	case AUTH_BASIC:
		return restGetBasicBodyBytes(method, url, authInfo.User, authInfo.Pass, defRestGetTO, bodyReq, magic)
	}
	// AUTH_PLAIN, default
	return restGetPlainBodyBytes(method, url, authInfo.Pass, defRestGetTO, bodyReq, magic)
}

func restGetOrPostWtMagicNFileExec(method, url string, authInfo AuthInfo, bodyReq io.Reader,
	magic []byte, fType, fName string, hdrs map[string]string) (bodyMap interface{}, errNo int, err error) {
	switch authInfo.Type {
	case AUTH_DIGEST:
		return restGetDigest(method, url, authInfo.User, authInfo.Pass, bodyReq, magic, fType, fName, hdrs)
	case AUTH_NONE:
		return restGetPlain(method, url, "", defRestGetTO, bodyReq, magic, fType, fName, hdrs)
	case AUTH_BASIC:
		return restGetBasic(method, url, authInfo.User, authInfo.Pass, defRestGetTO, bodyReq, magic, fType, fName, hdrs)
	}
	// AUTH_PLAIN, default
	return restGetPlain(method, url, authInfo.Pass, defRestGetTO, bodyReq, magic, fType, fName, hdrs)
}

/*
	RestGetOrPostBodyBytes sends Restful API request as a method and returns the result.

A magic string can be listed to be stripped in the beginning of the result.
*/
func RestGetOrPostSaveFile(method, url string, authInfo AuthInfo,
	magic []byte, file string) (errNo int, err error) {
	bodyBytes, errNo, err := restGetOrPostBodyBytes(method, url, authInfo, nil, magic)
	if err != nil {
		return
	}
	out, err := os.Create(file)
	if err != nil {
		return
	}
	defer out.Close()
	_, err = io.Copy(out, bytes.NewReader(bodyBytes))
	return
}

/*
	RestGetOrPostWtMagicNFileNHdr sends Restful API request as a method and returns the result.

Use this to specify a file with name and type to be sent as body, and/or extra headers.
If something wrong with the file, a request will be sent without it anyway.
A magic string can be listed to be stripped in the beginning of
the result. If magic string is assigned but not found in the result,
the result will not be parsed.
If failure response received, bodyMap is slice of byte, copying the failure body.
*/
func RestGetOrPostWtMagicNFileNHdr(method, url string, authInfo AuthInfo, magic []byte,
	fType, fName string, hdrs map[string]string) (bodyMap interface{}, errNo int, err error) {
	return restGetOrPostWtMagicNFileExec(method, url, authInfo, nil, magic, fType, fName, hdrs)
}

/*
	RestGetOrPostWtMagic sends Restful API request as a method and returns the result.

A magic string can be listed to be stripped in the beginning of
the result. If magic string is assigned but not found in the result,
the result will not be parsed.
If failure response received, bodyMap is slice of byte, copying the failure body.
*/
func RestGetOrPostWtMagic(method, url string, authInfo AuthInfo, bodyReq io.Reader, magic []byte) (bodyMap interface{}, errNo int, err error) {
	return restGetOrPostWtMagicNFileExec(method, url, authInfo, bodyReq, magic, "", "", nil)
}

/*
	RestPostWtMagic sends Restful API request as POST and returns the result.

A magic string can be listed to be stripped in the beginning of
the result. If magic string is assigned but not found in the result,
the result will not be parsed.
*/
func RestPostWtMagic(url string, authInfo AuthInfo, bodyReq io.Reader, magic []byte) (bodyMap interface{}, errNo int, err error) {
	return RestGetOrPostWtMagic(METHOD_POST, url, authInfo, bodyReq, magic)
}

/*
	RestGetWtMagic sends Restful API request as GET and returns the result.

A magic string can be listed to be stripped in the beginning of
the result. If magic string is assigned but not found in the result,
the result will not be parsed.
*/
func RestGetWtMagic(url string, authInfo AuthInfo, bodyReq io.Reader, magic []byte) (bodyMap interface{}, errNo int, err error) {
	return RestGetOrPostWtMagic(METHOD_GET, url, authInfo, bodyReq, magic)
}

// RestGet sends Restful API request and returns the result.
func RestGet(url string, authInfo AuthInfo, bodyReq io.Reader) (bodyMap interface{}, errNo int, err error) {
	return RestGetWtMagic(url, authInfo, bodyReq, nil)
}

/*
	RangeStrMap iterate through map[string]interface{} obj, calling fun for

each element recursively. When fun returns true, it stops.
false is returned if no element found.
*/
func RangeStrMap(obj interface{}, fun func(k string, v interface{}) bool) bool {
	//if the argument is not a map, ignore it
	mobj, ok := obj.(map[string]interface{})
	if !ok {
		return false
	}

	for k, v := range mobj {
		//key match, return value
		if fun(k, v) {
			return true
		}

		//if the value is a map, search recursively
		if m, ok := v.(map[string]interface{}); ok {
			if RangeStrMap(m, fun) {
				return true
			}
		}
		//if the value is an array, search recursively
		//from each element
		if va, ok := v.([]interface{}); ok {
			for _, a := range va {
				if RangeStrMap(a, fun) {
					return true
				}
			}
		}
	}

	//element not found
	return false
}

/*
	FindStrMap find string key in map[string]interface{} obj,

returning the value and true or nil and false.
*/
func FindStrMap(obj interface{}, key string) (interface{}, bool) {
	//if the argument is not a map, ignore it
	mobj, ok := obj.(map[string]interface{})
	if !ok {
		return nil, false
	}

	for k, v := range mobj {
		//key match, return value
		if k == key {
			return v, true
		}

		//if the value is a map, search recursively
		if m, ok := v.(map[string]interface{}); ok {
			if res, ok := FindStrMap(m, key); ok {
				return res, true
			}
		}
		//if the value is an array, search recursively
		//from each element
		if va, ok := v.([]interface{}); ok {
			for _, a := range va {
				if res, ok := FindStrMap(a, key); ok {
					return res, true
				}
			}
		}
	}

	//element not found
	return nil, false
}
